<script>
  import { beforeUrlChange, goto, params } from "@roxi/routify"
  import { datasources, flags, integrations, queries } from "@/stores/builder"
  import { consumeSkipUnsavedPrompt } from "@/stores/builder/queries"

  import {
    Banner,
    Body,
    Button,
    Divider,
    Heading,
    Input,
    Label,
    Layout,
    notifications,
    RadioGroup,
    Select,
    Tab,
    Table,
    Tabs,
    TextArea,
  } from "@budibase/bbui"
  import KeyValueBuilder from "@/components/integration/KeyValueBuilder.svelte"
  import EditableLabel from "@/components/common/inputs/EditableLabel.svelte"
  import CodeMirrorEditor, {
    EditorModes,
  } from "@/components/common/CodeMirrorEditor.svelte"
  import RestBodyInput from "./RestBodyInput.svelte"
  import { capitalise, confirm } from "@/helpers"
  import { onMount } from "svelte"
  import restUtils, { customQueryIconColor } from "@/helpers/data/utils"
  import {
    PaginationLocations,
    PaginationTypes,
    RawRestBodyTypes,
    RestBodyTypes as bodyTypes,
    SchemaTypeOptionsExpanded,
  } from "@/constants/backend"
  import JSONPreview from "@/components/integration/JSONPreview.svelte"
  import AccessLevelSelect from "@/components/integration/AccessLevelSelect.svelte"
  import DynamicVariableModal from "./DynamicVariableModal.svelte"
  import Placeholder from "assets/bb-spaceship.svg"
  import { cloneDeep } from "lodash/fp"

  import {
    getRestBindings,
    readableToRuntimeBinding,
    readableToRuntimeMap,
    runtimeToReadableMap,
    toBindingsArray,
  } from "@/dataBinding"
  import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
  import AuthPicker from "./rest/AuthPicker.svelte"
  import {
    getBindingContext,
    prettifyQueryRequestBody,
    keyValueArrayToRecord,
  } from "./query"

  export let queryId
  let lastViewedQueryId = null

  let query, datasource
  let breakQs = {},
    requestBindings = {}
  let saveId
  let response, schema, enabledHeaders
  let dynamicVariables = {},
    addVariableModal,
    varBinding,
    globalDynamicBindings = {}
  let restBindings = getRestBindings()
  let nestedSchemaFields = {}
  let saving
  let queryNameLabel
  let mounted = false
  let isTemplateDatasource = false

  $: staticVariables = datasource?.config?.staticVariables || {}
  $: if (queryId) {
    lastViewedQueryId = queryId
  }

  $: customRequestBindings = toBindingsArray(
    requestBindings,
    "Binding",
    "Bindings"
  )
  $: globalDynamicRequestBindings = toBindingsArray(
    globalDynamicBindings,
    "Dynamic",
    "Dynamic"
  )
  $: dataSourceStaticBindings = toBindingsArray(
    staticVariables,
    "Datasource.Static",
    "Datasource Static"
  )

  $: mergedBindings = [
    ...dataSourceStaticBindings,
    ...restBindings,
    ...customRequestBindings,
    ...globalDynamicRequestBindings,
  ]

  $: isTemplateDatasource = Boolean(datasource?.restTemplate)
  $: bindingPreviewContext = getBindingContext([
    requestBindings,
    globalDynamicBindings,
    dynamicVariables,
    staticVariables,
  ])

  $: datasourceType = datasource?.source
  $: integrationInfo = $integrations[datasourceType]
  $: queryConfig = integrationInfo?.query
  $: verbOptions = Object.keys(queryConfig || {}).map(verb => {
    const label = queryConfig?.[verb]?.displayName || capitalise(verb)
    return {
      value: verb,
      label,
      colour: customQueryIconColor(verb),
    }
  })
  $: url = buildUrl(query?.fields?.path, breakQs)
  $: checkQueryName(url)
  $: responseSuccess = response?.info?.code >= 200 && response?.info?.code < 400
  $: isGet = query?.queryVerb === "read"
  $: authConfigs = buildAuthConfigs(datasource)
  $: schemaReadOnly = !responseSuccess
  $: variablesReadOnly = !responseSuccess
  $: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly)
  $: hasSchema = Object.keys(schema || {}).length !== 0

  $: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)

  $: builtQuery = buildQuery(query, runtimeUrlQueries, requestBindings)
  $: originalQuery = mounted
    ? (originalQuery ?? cloneDeep(builtQuery))
    : undefined
  $: isModified = JSON.stringify(originalQuery) !== JSON.stringify(builtQuery)
  $: prettyBody = query?.fields?.requestBody
    ? prettifyQueryRequestBody(query, mergedBindings)
    : undefined

  function getSelectedQuery() {
    return cloneDeep(
      $queries.list.find(q => q._id === queryId) || {
        datasourceId: $params.datasourceId,
        parameters: [],
        fields: {
          // only init the objects, everything else is optional strings
          disabledHeaders: {},
          headers: {},
        },
        queryVerb: "read",
      }
    )
  }

  const cleanUrl = inputUrl =>
    url
      ?.replace(/(https)|(http)|[{}:]/g, "")
      ?.replaceAll(".", "_")
      ?.replaceAll("/", " ")
      ?.trim() || inputUrl

  function checkQueryName(inputUrl = null) {
    if (query && (!query.name || query.flags?.urlName)) {
      query.flags ??= {}
      query.flags.urlName = true
      query.name = cleanUrl(inputUrl)
    }
  }

  function buildUrl(base, qsObj) {
    if (!base) {
      return base
    }
    let qs = restUtils.buildQueryString(
      runtimeToReadableMap(mergedBindings, qsObj)
    )
    let newUrl = base
    if (base.includes("?")) {
      const split = base.split("?")
      newUrl = split[0]
    }
    return qs.length === 0 ? newUrl : `${newUrl}?${qs}`
  }

  function buildQuery(fromQuery, urlQueries, requestBindings) {
    if (!fromQuery) {
      return
    }
    const newQuery = cloneDeep(fromQuery)
    const queryString = restUtils.buildQueryString(urlQueries)

    newQuery.parameters = restUtils.keyValueToQueryParameters(requestBindings)
    newQuery.fields.requestBody =
      typeof newQuery.fields.requestBody === "object"
        ? readableToRuntimeMap(mergedBindings, newQuery.fields.requestBody)
        : readableToRuntimeBinding(mergedBindings, newQuery.fields.requestBody)

    newQuery.fields.path = fromQuery.fields.path
    newQuery.fields.queryString = queryString
    newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders)
    newQuery.schema = schema || {}
    newQuery.nestedSchemaFields = nestedSchemaFields || {}

    return newQuery
  }

  function onUpdateBody(e) {
    if (query) {
      query.fields.requestBody = e.detail.requestBody
    }
  }

  async function saveQuery(redirectIfNew = true) {
    const toSave = builtQuery
    saving = true
    try {
      const isNew = !query._rev
      const { _id } = await queries.save(toSave.datasourceId, toSave)
      saveId = _id
      if (dynamicVariables) {
        datasource.config.dynamicVariables = rebuildVariables(saveId)
        datasource = await datasources.save({
          integration: integrationInfo,
          datasource,
        })
      }

      notifications.success(`Request saved successfully`)
      if (isNew && redirectIfNew) {
        isModified = false
        $goto(`../../${_id}`)
      }

      query = getSelectedQuery()
      query.fields.requestBody = prettifyQueryRequestBody(query, mergedBindings)

      // Force rebuilding original query
      originalQuery = null

      queryNameLabel.disableEditingState()
      return { ok: true }
    } catch (err) {
      notifications.error(`Error saving query`)
    } finally {
      saving = false
    }

    return { ok: false }
  }

  const validateQuery = async () => {
    const forbiddenBindings = /{{\s?user(\.(\w|\$)*\s?|\s?)}}/g
    const bindingError = new Error(
      "'user' is a protected binding and cannot be used"
    )

    if (forbiddenBindings.test(url)) {
      throw bindingError
    }

    if (forbiddenBindings.test(query.fields.requestBody ?? "")) {
      throw bindingError
    }

    Object.values(requestBindings).forEach(bindingValue => {
      if (forbiddenBindings.test(bindingValue)) {
        throw bindingError
      }
    })

    Object.values(query.fields.headers).forEach(headerValue => {
      if (forbiddenBindings.test(headerValue)) {
        throw bindingError
      }
    })
  }

  async function runQuery() {
    try {
      await validateQuery()
      response = await queries.preview(builtQuery)
      if (response.rows.length === 0) {
        notifications.info("Request did not return any data")
      } else {
        response.info = response.info || { code: 200 }
        // if existing schema, copy over what it is
        if (schema) {
          for (let [name, field] of Object.entries(response.schema)) {
            if (!schema[name]) {
              schema[name] = field
            }
          }
        }
        schema = response.schema
        nestedSchemaFields = response.nestedSchemaFields
        notifications.success("Request sent successfully")
      }
    } catch (error) {
      notifications.error(`Query Error: ${error.message}`)
    }
  }

  const buildAuthConfigs = datasource => {
    if (datasource?.config?.authConfigs) {
      return datasource.config.authConfigs.map(c => ({
        label: c.name,
        value: c._id,
      }))
    }
    return []
  }

  const schemaMenuItems = [
    {
      text: "Create dynamic variable",
      onClick: input => {
        varBinding = `{{ data.0.[${input.name}] }}`
        addVariableModal.show()
      },
    },
  ]
  const responseHeadersMenuItems = [
    {
      text: "Create dynamic variable",
      onClick: input => {
        varBinding = `{{ info.headers.[${input.name}] }}`
        addVariableModal.show()
      },
    },
  ]

  // convert dynamic variables list to simple key/val object
  const getDynamicVariables = (datasource, queryId, matchFn) => {
    const variablesList = datasource?.config?.dynamicVariables
    if (variablesList && variablesList.length > 0) {
      const filtered = queryId
        ? variablesList.filter(variable => matchFn(variable, queryId))
        : variablesList
      return filtered.reduce(
        (acc, next) => ({ ...acc, [next.name]: next.value }),
        {}
      )
    }
    return {}
  }

  // convert dynamic variables object back to a list, enrich with query id
  const rebuildVariables = queryId => {
    let variables = []
    if (dynamicVariables) {
      variables = Object.entries(dynamicVariables).map(entry => {
        return {
          name: entry[0],
          value: entry[1],
          queryId,
        }
      })
    }

    let existing = datasource?.config?.dynamicVariables || []
    // remove existing query variables (for changes and deletions)
    existing = existing.filter(variable => variable.queryId !== queryId)
    // re-add the new query variables
    return [...existing, ...variables]
  }

  const shouldShowVariables = (dynamicVariables, variablesReadOnly) => {
    return !!(
      dynamicVariables &&
      // show when editable or when read only and not empty
      (!variablesReadOnly || Object.keys(dynamicVariables).length > 0)
    )
  }

  const updateFlag = async (flag, value) => {
    try {
      await flags.updateFlag(flag, value)
    } catch (error) {
      notifications.error("Error updating flag")
    }
  }

  const urlChanged = evt => {
    if (isTemplateDatasource) {
      return
    }
    breakQs = {}
    const fullUrl = evt.detail
    if (!fullUrl) return

    const [basePath, qs] = fullUrl?.split("?") || []

    // Update the query fields path with just the base path
    if (query?.fields) {
      query.fields.path = basePath
    }

    // Parse query parameters
    if (qs && qs.length > 0) {
      const parts = qs.split("&")
      for (let part of parts) {
        const [key, value] = part.split("=")
        if (key) {
          breakQs[key] = value || ""
        }
      }
    }
  }

  onMount(async () => {
    query = getSelectedQuery()
    schema = query.schema

    try {
      // Clear any unsaved changes to the datasource
      await datasources.init()
    } catch (error) {
      notifications.error("Error getting datasources")
    }

    datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
    const datasourceUrl = datasource?.config.url
    const qs = query?.fields.queryString
    breakQs = restUtils.breakQueryString(encodeURI(qs ?? ""))
    breakQs = runtimeToReadableMap(mergedBindings, breakQs)

    const path = query.fields.path
    if (
      datasourceUrl &&
      !path?.startsWith("http") &&
      !path?.startsWith("{{") // don't substitute the datasource url when query starts with a variable e.g. the upgrade path
    ) {
      query.fields.path = `${datasource.config.url}/${path ? path : ""}`
    }
    requestBindings = restUtils.queryParametersToKeyValue(query.parameters)
    if (!query.fields.disabledHeaders) {
      query.fields.disabledHeaders = {}
    }
    // make sure the disabled headers are set (migration)
    for (let header of Object.keys(query.fields.headers)) {
      if (!query.fields.disabledHeaders[header]) {
        query.fields.disabledHeaders[header] = false
      }
    }
    enabledHeaders = restUtils.flipHeaderState(query.fields.disabledHeaders)
    if (query && !query.transformer) {
      query.transformer = "return data"
    }
    if (query && !query.flags) {
      query.flags = {
        urlName: false,
      }
    }
    if (query && !query.fields.bodyType) {
      if (query.fields.requestBody) {
        query.fields.bodyType = RawRestBodyTypes.JSON
      } else {
        query.fields.bodyType = RawRestBodyTypes.NONE
      }
    }
    if (query && !query.fields.pagination) {
      query.fields.pagination = {}
    }
    // if query doesn't have ID then its new - don't try to copy existing dynamic variables
    if (!queryId) {
      dynamicVariables = {}
      globalDynamicBindings = getDynamicVariables(datasource)
    } else {
      dynamicVariables = getDynamicVariables(
        datasource,
        query._id,
        (variable, queryId) => variable.queryId === queryId
      )
      globalDynamicBindings = getDynamicVariables(
        datasource,
        query._id,
        (variable, queryId) => variable.queryId !== queryId
      )
    }

    query.fields.requestBody = prettifyQueryRequestBody(query, mergedBindings)

    mounted = true
  })

  $beforeUrlChange(async () => {
    if (!isModified || consumeSkipUnsavedPrompt(lastViewedQueryId)) {
      return true
    }

    return await confirm({
      title: "Some updates are not saved",
      body: "Some of your changes are not yet saved. Do you want to save them before leaving?",
      okText: "Save and continue",
      cancelText: "Discard and continue",
      size: "M",
      onConfirm: async () => {
        const saveResult = await saveQuery(false)
        if (!saveResult.ok) {
          // We can't leave as the query was not properly saved
          return false
        }

        return true
      },
      onCancel: () => {
        // Leave without saving anything
        return true
      },
      onClose: () => {
        return false
      },
    })
  })
</script>

<DynamicVariableModal
  {datasource}
  {dynamicVariables}
  bind:binding={varBinding}
  bind:this={addVariableModal}
  on:change={saveQuery}
/>
{#if query && queryConfig}
  <div class="inner">
    <div class="top">
      <Layout gap="S">
        <div class="top-bar">
          <EditableLabel
            bind:this={queryNameLabel}
            type="heading"
            bind:value={query.name}
            defaultValue="Untitled"
            on:change={() => (query.flags.urlName = false)}
            on:save={saveQuery}
          />
          <div class="controls">
            {#if query._id}
              <ConnectedQueryScreens sourceId={query._id} />
            {/if}
            <div class="access">
              <Label>Access</Label>
              <AccessLevelSelect {query} {saveId} />
            </div>
          </div>
        </div>
        <div class="url-block">
          <div class="verb">
            <Select
              bind:value={query.queryVerb}
              on:change={() => {}}
              options={verbOptions}
              getOptionValue={option => option.value}
              getOptionLabel={option => option.label}
              getOptionColour={option => option.colour}
              readonly={isTemplateDatasource}
              hideChevron={isTemplateDatasource}
            />
          </div>
          <div class="url">
            <Input
              on:blur={urlChanged}
              value={url}
              placeholder="http://www.api.com/endpoint"
              readonly={isTemplateDatasource}
            />
          </div>
          <Button primary disabled={!url} on:click={runQuery}>Send</Button>
          <Button
            disabled={!query.name || !isModified || saving}
            cta
            on:click={saveQuery}
            tooltip={!hasSchema
              ? "Saving a query before sending will mean no schema is generated"
              : null}
            >Save
          </Button>
        </div>
        <Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
          <Tab title="Bindings">
            <KeyValueBuilder
              bind:object={requestBindings}
              tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query"
              name="binding"
              headings
              keyPlaceholder="Binding name"
              valuePlaceholder="Default"
              bindings={[
                ...dataSourceStaticBindings,
                ...restBindings,
                ...globalDynamicRequestBindings,
              ]}
              context={bindingPreviewContext}
            />
          </Tab>
          <Tab title="Params">
            {#key breakQs}
              <KeyValueBuilder
                name="param"
                defaults={breakQs}
                headings
                bindings={mergedBindings}
                context={bindingPreviewContext}
                on:change={e => {
                  breakQs = keyValueArrayToRecord(e.detail)
                }}
              />
            {/key}
          </Tab>
          <Tab title="Headers">
            <KeyValueBuilder
              bind:object={query.fields.headers}
              bind:activity={enabledHeaders}
              toggle
              name="header"
              headings
              bindings={mergedBindings}
              context={bindingPreviewContext}
            />
          </Tab>
          <Tab title="Body">
            <RadioGroup
              bind:value={query.fields.bodyType}
              options={isGet ? [bodyTypes[0]] : bodyTypes}
              direction="horizontal"
              getOptionLabel={option => option.name}
              getOptionValue={option => option.value}
            />
            <RestBodyInput
              bodyType={query.fields.bodyType}
              requestBody={prettyBody}
              on:change={onUpdateBody}
            />
          </Tab>
          <Tab title="Pagination">
            <div class="pagination">
              <Select
                label="Pagination type"
                bind:value={query.fields.pagination.type}
                options={PaginationTypes}
                placeholder="None"
              />
              {#if query.fields.pagination.type}
                <Select
                  label="Pagination parameters location"
                  bind:value={query.fields.pagination.location}
                  options={PaginationLocations}
                  placeholer="Choose where to send pagination parameters"
                />
                <Input
                  label={query.fields.pagination.type === "page"
                    ? "Page number parameter name "
                    : "Request cursor parameter name"}
                  bind:value={query.fields.pagination.pageParam}
                />
                <Input
                  label={query.fields.pagination.type === "page"
                    ? "Page size parameter name"
                    : "Request limit parameter name"}
                  bind:value={query.fields.pagination.sizeParam}
                />
                {#if query.fields.pagination.type === "cursor"}
                  <Input
                    label="Response body parameter name for cursor"
                    bind:value={query.fields.pagination.responseParam}
                  />
                {/if}
              {/if}
            </div>
          </Tab>
          <Tab title="Transformer">
            <Layout noPadding>
              {#if !$flags.queryTransformerBanner}
                <Banner
                  extraButtonText="Learn more"
                  extraButtonAction={() =>
                    window.open("https://docs.budibase.com/docs/transformers")}
                  on:change={() => updateFlag("queryTransformerBanner", true)}
                >
                  Add a JavaScript function to transform the query result.
                </Banner>
              {/if}
              <CodeMirrorEditor
                height={200}
                mode={EditorModes.JSON}
                value={query.transformer}
                resize="vertical"
                on:change={e => (query.transformer = e.detail)}
              />
            </Layout>
          </Tab>
          <div class="auth-container">
            <div />
            <!-- spacer -->

            <AuthPicker
              bind:authConfigId={query.fields.authConfigId}
              bind:authConfigType={query.fields.authConfigType}
              {authConfigs}
              datasourceId={datasource._id}
            />
          </div>
        </Tabs>
      </Layout>
    </div>
    <div class="bottom">
      <Layout paddingY="S" gap="S">
        <Divider />
        {#if !response && Object.keys(schema || {}).length === 0}
          <Heading size="M">Response</Heading>
          <div class="placeholder">
            <div class="placeholder-internal">
              <img alt="placeholder" src={Placeholder} />
              <Body size="XS" textAlign="center"
                >{"enter a url in the textbox above and click send to get a response".toUpperCase()}</Body
              >
            </div>
          </div>
        {:else}
          <Tabs
            selected={!response ? "Schema" : "JSON"}
            quiet
            noPadding
            noHorizPadding
          >
            {#if response}
              <Tab title="JSON">
                <div>
                  <JSONPreview height={300} data={response.rows[0]} />
                </div>
              </Tab>
            {/if}
            {#if schema || response}
              <Tab title="Schema">
                <KeyValueBuilder
                  bind:object={schema}
                  name="schema"
                  headings
                  options={SchemaTypeOptionsExpanded}
                  menuItems={schemaMenuItems}
                  showMenu={!schemaReadOnly}
                  readOnly={schemaReadOnly}
                  compare={(option, value) => option.type === value?.type}
                />
              </Tab>
            {/if}
            {#if response}
              <Tab title="Raw">
                <TextArea disabled value={response.extra?.raw} height={300} />
              </Tab>
              <Tab title="Headers">
                <KeyValueBuilder
                  object={response.extra?.headers}
                  readOnly
                  menuItems={responseHeadersMenuItems}
                  showMenu={true}
                />
              </Tab>
              <Tab title="Preview">
                <div class="table">
                  {#if response}
                    <Table
                      schema={response?.schema}
                      data={response?.rows}
                      allowEditColumns={false}
                      allowEditRows={false}
                      allowSelectRows={false}
                    />
                  {/if}
                </div>
              </Tab>
            {/if}
            {#if showVariablesTab}
              <Tab title="Dynamic Variables">
                <Layout noPadding gap="S">
                  <Body size="S">
                    Create dynamic variables based on response body or headers
                    from this query.
                  </Body>
                  <KeyValueBuilder
                    bind:object={dynamicVariables}
                    name="Variable"
                    headings
                    keyHeading="Name"
                    keyPlaceholder="Variable name"
                    valueHeading={`Value`}
                    valuePlaceholder={`{{ value }}`}
                    readOnly={variablesReadOnly}
                  />
                </Layout>
              </Tab>
            {/if}
            {#if response}
              <div class="stats">
                <Label size="L">
                  Status: <span class={responseSuccess ? "green" : "red"}
                    >{response?.info.code}</span
                  >
                </Label>
                <Label size="L">
                  Time: <span class={responseSuccess ? "green" : "red"}
                    >{response?.info.time}</span
                  >
                </Label>
                <Label size="L">
                  Size: <span class={responseSuccess ? "green" : "red"}
                    >{response?.info.size}</span
                  >
                </Label>
              </div>
            {/if}
          </Tabs>
        {/if}
      </Layout>
    </div>
  </div>
{/if}

<style>
  .inner {
    width: 960px;
    margin: 0 auto;
    height: 100%;
  }

  .table {
    width: 960px;
  }

  .url-block {
    display: flex;
    gap: var(--spacing-s);
    z-index: 200;
  }

  .verb {
    flex: 1;
  }

  .url {
    flex: 4;
  }

  .top {
    min-height: 50%;
  }

  .bottom {
    padding-bottom: 50px;
  }

  .stats {
    display: flex;
    gap: var(--spacing-xl);
    margin-left: auto !important;
    margin-right: 0;
    align-items: center;
  }

  .green {
    color: #53a761;
  }

  .red {
    color: #ea7d82;
  }

  .top-bar {
    display: flex;
    justify-content: space-between;
  }

  .controls {
    display: flex;
    align-items: center;
    gap: var(--spacing-m);
  }

  .access {
    display: flex;
    gap: var(--spacing-m);
    align-items: center;
  }

  .placeholder-internal {
    display: flex;
    flex-direction: column;
    width: 200px;
    gap: var(--spacing-l);
  }

  .placeholder {
    display: flex;
    margin-top: var(--spacing-xl);
    justify-content: center;
  }

  .auth-container {
    width: 100%;
    display: flex;
    justify-content: space-between;
  }

  .pagination {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--spacing-m);
  }
</style>
